1 /*
2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
3 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
4 Authors: Marcelo S. N. Mancini
5 
6 	Copyright Marcelo S. N. Mancini 2018 - 2021.
7 Distributed under the CC BY-4.0 License.
8    (See accompanying file LICENSE.txt or copy at
9 	https://creativecommons.org/licenses/by/4.0/
10 */
11 module hip.assets.textureatlas;
12 import hip.asset;
13 public import hip.api.data.textureatlas;
14 
15 
16 class HipTextureAtlas : HipAsset, IHipTextureAtlas
17 {
18     import hip.assets.image;
19     string atlasPath;
20     string[] texturePaths;
21     IHipTexture texture;
22     AtlasFrame[string] _frames;
23 
24     ref AtlasFrame[string] frames(){return _frames;}
25 
26     this()
27     {
28         super("TextureAtlas");
29         _typeID = assetTypeID!HipTextureAtlas;
30     }
31 
32     string getTexturePath () const
33     {
34         return texturePaths[0];
35     }
36 
37 
38     bool loadTexture (in Image image)
39     {
40         import hip.assets.texture;
41         texture = new HipTexture(image);
42         if(!texture.hasSuccessfullyLoaded)
43             return false;
44         foreach(k, ref v; frames)
45         {
46             v.region = new HipTextureRegion(texture, 
47                 cast(uint)v.frame.x, cast(uint)v.frame.y, 
48                 cast(uint)(v.frame.x + v.frame.width),
49                 cast(uint)(v.frame.y + v.frame.height)
50             );
51         }
52         return true;
53     }
54 
55     static HipTextureAtlas readJSON (ubyte[] data, string atlasPath, string texturePath)
56     {
57         import hip.data.json;
58         import hip.assets.texture;
59         HipTextureAtlas ret = new HipTextureAtlas();
60         ret.texturePaths~= texturePath;
61         ret.atlasPath = atlasPath;
62 
63         import hip.console.log;
64 
65         JSONValue json = parseJSON(cast(string)data);
66         if(json["frames"].type == JSONType.array)
67         {
68             foreach(f; json["frames"].array)
69             {
70                 AtlasFrame a;
71                 a.filename = f["filename"].str;
72                 a.rotated  = f["rotated"].boolean;
73                 a.trimmed  = f["trimmed"].boolean;
74                 JSONValue frameRect = f["frame"].object;
75                 a.frame = AtlasRect(
76                     cast(uint)frameRect["x"].integer,
77                     cast(uint)frameRect["y"].integer,
78                     cast(uint)frameRect["w"].integer,
79                     cast(uint)frameRect["h"].integer
80                 );
81                 frameRect = f["spriteSourceSize"].object;
82                 a.spriteSourceSize = AtlasRect(
83                     cast(uint)frameRect["x"].integer,
84                     cast(uint)frameRect["y"].integer,
85                     cast(uint)frameRect["w"].integer,
86                     cast(uint)frameRect["h"].integer
87                 );
88                 frameRect = f["sourceSize"].object;
89                 a.sourceSize = AtlasSize(cast(uint)frameRect["w"].integer, cast(uint)frameRect["h"].integer);
90                 ret.frames[a.filename] = a;
91             }
92         }
93         else 
94         {
95             JSONValue frames = json["frames"].object;
96             JSONValue meta = json["meta"].object;
97             foreach(string frameName, JSONValue f; frames)
98             {
99                 AtlasFrame a;
100                 a.filename = frameName;
101                 a.rotated  = f["rotated"].boolean;
102                 a.trimmed  = f["trimmed"].boolean;
103                 JSONValue frameRect = f["frame"].object;
104                 a.frame = AtlasRect(
105                     cast(uint)frameRect["x"].integer,
106                     cast(uint)frameRect["y"].integer,
107                     cast(uint)frameRect["w"].integer,
108                     cast(uint)frameRect["h"].integer
109                 );
110                 frameRect = f["spriteSourceSize"].object;
111                 a.spriteSourceSize = AtlasRect(
112                     cast(uint)frameRect["x"].integer,
113                     cast(uint)frameRect["y"].integer,
114                     cast(uint)frameRect["w"].integer,
115                     cast(uint)frameRect["h"].integer
116                 );
117                 frameRect = f["sourceSize"].object;
118                 a.sourceSize = AtlasSize(cast(uint)frameRect["w"].integer, cast(uint)frameRect["h"].integer);
119                 ret.frames[frameName] = a;   
120             }
121         }
122 
123         return ret;
124     }
125 
126     /**
127     *   If no texturePath is given, it will try spritesheetPath<.png>
128     *   I found a txt file that is parsed as:
129     *
130 `spriteName = x y width height`
131     */
132     static HipTextureAtlas readSpritesheet (string spritesheetPath, string texturePath = "")
133     {
134         import hip.filesystem.hipfs;
135         string data;
136         if(!HipFS.readText(spritesheetPath))
137         {
138             import hip.error.handler;
139             ErrorHandler.showWarningMessage("Could not find spritesheet from path ", spritesheetPath);
140             return null;
141         }
142         import hip.util.path;
143         if(texturePath == "")
144         {
145             texturePath = spritesheetPath.dup.extension(".png");
146         }
147 
148         return readSpritesheet(cast(ubyte[])data, spritesheetPath, texturePath);
149     }
150 
151     static HipTextureAtlas readSpritesheet (ubyte[] data, string spritesheetPath, string texturePath)
152     {
153         import hip.util.string:splitRange, trim, isNumber;
154         import hip.assets.texture;
155         
156         string toParse = cast(string)data;
157 
158         HipTextureAtlas atlas = new HipTextureAtlas();
159         atlas.atlasPath = spritesheetPath;
160         atlas.texturePaths~= texturePath;
161 
162 
163         foreach(line; splitRange(toParse, "\n"))
164         {
165             import hip.util.algorithm;
166             import hip.util.conv;
167             import hip.console.log;
168             line = line.trim();
169             auto lineDataRange = splitRange(line, " ");
170             lineDataRange.popFront;
171             string frame = lineDataRange.front;
172             if(frame != "")
173             {
174                 while(!lineDataRange.empty && !lineDataRange.front.isNumber)
175                 {
176                     lineDataRange.popFront; //Find a number
177                 }
178                 int x = void, y = void, width = void, height = void;
179                 lineDataRange.map((string data) => to!int(data)).put(&x, &y, &width, &height);
180                 AtlasFrame atlasFrame;
181                 atlasFrame.spriteSourceSize = AtlasRect(x, y, width, height);
182                 atlasFrame.frame = AtlasRect(x, y, width, height);
183                 atlasFrame.sourceSize = AtlasSize(width, height);
184                 atlasFrame.filename = frame;
185                 atlas.frames[frame] = atlasFrame;
186             }
187         }
188         return atlas;
189     }
190 
191 
192     static HipTextureAtlas readFromMemory (ubyte[] data, string atlasPath, string texturePath = ":IGNORE")
193     {
194         import hip.util.path;
195         switch(atlasPath.extension)
196         {
197             case "xml":
198                 return HipTextureAtlas.readXML(data, atlasPath, texturePath);
199             case "atlas":
200                 return HipTextureAtlas.readAtlas(data, atlasPath);
201             case "json":
202                 return HipTextureAtlas.readJSON(data, atlasPath, texturePath == ":IGNORE" ? "" : texturePath);
203             default:
204                 return HipTextureAtlas.readSpritesheet(data, atlasPath, texturePath == ":IGNORE" ? "" : texturePath);
205         }
206     }
207 
208     /**
209     *   Used for the following type of XML (Parsed without a real XML parser):
210     ```xml
211        <TextureAtlas imagePath="image.png">
212            <SubTexture name="sub.png" x="0" y="0" width="0" height="0"/>
213        </TextureAtlas>
214     ```
215     */
216     static HipTextureAtlas readXML (ubyte[] data, string atlasPath, string texturePath = ":IGNORE")
217     {
218         import hip.assets.texture;
219         import hip.util.string;
220         import hip.util.path;
221         import hip.util.conv;
222         string dataToParse = cast(string)data;
223         import hip.console.log;
224         //TODO: Fix .after (as it only executes startsWith and is returning null)
225         dataToParse = dataToParse.findAfter("imagePath=");
226         if(texturePath == ":IGNORE")
227             texturePath = atlasPath.dup.extension(".png");
228         else
229             texturePath = dataToParse.between("\"", "\"");
230         HipTextureAtlas atlas = new HipTextureAtlas();
231         atlas.texturePaths~= texturePath;
232         dataToParse = dataToParse.findAfter(">");
233 
234         foreach(line; dataToParse.splitRange("\n"))
235         {
236             line = line.trim();
237             if(!line)
238                 continue;
239             if(line.startsWith("</TextureAtlas>"))
240                 break;
241             string name = (line = line.findAfter("name=")).between(`"`, `"`);
242             int x = (line = line.findAfter("x=")).between(`"`, `"`).to!int;
243             int y = (line = line.findAfter("y=")).between(`"`, `"`).to!int;
244             int width = (line = line.findAfter("width=")).between(`"`, `"`).to!int;
245             int height = (line = line.findAfter("height=")).between(`"`, `"`).to!int;
246 
247             AtlasFrame frame;
248             frame.filename = name;
249             frame.frame = AtlasRect(x, y, width, height);
250             frame.spriteSourceSize = AtlasRect(x, y, width, height);
251             frame.sourceSize = AtlasSize(width, height);
252             atlas.frames[frame.filename] = frame;
253         }
254         return atlas;
255     }
256     
257 
258     static HipTextureAtlas readAtlas (ubyte[] data, string atlasPath)
259     {
260         import hip.util.string : split, countUntil;
261         import hip.util.conv : to;
262 
263         HipTextureAtlas ret = new HipTextureAtlas();
264         ret.atlasPath = atlasPath;
265         string atlasFile = cast(string)data;
266 
267         string[] lines = atlasFile.split("\n");
268         int i = 0;
269         while(lines[i] == "")
270             i++;
271         string textureName = lines[i++];
272         ret.texturePaths~= textureName;
273         string sizeText = lines[i++];
274         string format = lines[i++];
275         string filter = lines[i++];
276         string repeat = lines[i++];
277     
278         const int offset = i;
279 
280         for(; i < lines.length-offset; i+= 7)
281         {
282             AtlasFrame frame;
283             frame.trimmed = false;
284             frame.filename = lines[i];
285 
286             string rotate = lines[i+1];
287                 rotate = rotate[rotate.countUntil(":")+2 .. $];
288             frame.rotated = to!bool(rotate);
289 
290             string xy = lines[i+2];
291                 xy = xy[xy.countUntil(":")+2 .. $];
292 
293             ptrdiff_t commaIndex = xy.countUntil(',');
294             int x = to!int(xy[0..commaIndex]);
295             //To account space must increate 2
296             int y = to!int(xy[commaIndex+2..$]);
297             
298             string size = lines[i+3];
299                 size = size[size.countUntil(":")+2 .. $];
300 
301             commaIndex = size.countUntil(',');
302             int sizeW = to!int(size[0..commaIndex]);
303             int sizeH = to!int(size[commaIndex+2..$]);
304 
305             string orig = lines[i+4];
306                 orig = orig[orig.countUntil(":")+2 .. $];
307 
308             commaIndex = orig.countUntil(',');
309             int origX = to!int(orig[0..commaIndex]);
310             int origY = to!int(orig[commaIndex+2..$]);
311 
312             string _offset = lines[i+5];
313                 _offset = _offset[_offset.countUntil(":")+2 .. $];
314 
315             commaIndex = _offset.countUntil(',');
316             int _offsetX = to!int(_offset[0..commaIndex]);
317             int _offsetY = to!int(_offset[commaIndex+2..$]);
318 
319             string index = lines[i+6];
320                 index = index[index.countUntil(":")+2 .. $];
321 
322             frame.frame = AtlasRect(x, y, sizeW, sizeH);
323             frame.spriteSourceSize = AtlasRect(_offsetX, _offsetY, sizeW, sizeH);
324             frame.sourceSize = AtlasSize(sizeW, sizeH);
325             ret.frames[frame.filename] = frame;
326         }
327         return ret;
328     }
329 
330     
331     override void onFinishLoading(){}
332     override void onDispose(){}
333     bool isReady(){return texture !is null && frames.length > 0;}
334     
335 
336     alias frames this;
337 }